Skip to content

feat: move dynamic Discord roles from env vars to database#179

Merged
dimoschi merged 15 commits intomainfrom
feature/dynamic-roles-db
Apr 6, 2026
Merged

feat: move dynamic Discord roles from env vars to database#179
dimoschi merged 15 commits intomainfrom
feature/dynamic-roles-db

Conversation

@dimoschi
Copy link
Copy Markdown
Contributor

@dimoschi dimoschi commented Apr 3, 2026

Summary

  • Moves dynamic Discord roles (ranks, certs, seasons, subscriptions, creators, positions, joinable) from required env vars to a DB-backed DynamicRole table with in-memory cache
  • Core roles (moderation, permissions) remain as required env vars since they're used in decorators at import time
  • Adds /admin role CRUD commands so bot admins can manage roles at runtime without redeployment
  • New academy certs, ranks, etc. require zero code changes, just an /admin role add command

Motivation

PR #177 added ACADEMY_CWPE as a required env var but it was forgotten in production config, crashing the bot on deploy. This makes the bot resilient to missing dynamic roles (graceful degradation) and eliminates the need for code changes when adding new roles.

Key Design Decisions

  • Race condition prevention: RoleManager loads synchronously in __main__.py before either the webhook server or Discord bot starts
  • Dual-read fallback: Dynamic role fields are Optional[int] = None in the Roles class. RoleManager tries DB first, falls back to env vars if set. Safe to deploy with empty DB table.
  • Cert mappings fully in DB: cert_full_name and cert_integer_id columns on academy_cert rows replace the hardcoded AcademyCertificates class and process_certification() mapping
  • Autocomplete for /join and /leave: Replaces static choices= with dynamic autocomplete from DB
  • Extensible admin structure: /admin group defined in admin.py, subgroups added via admin.create_subgroup()

New Commands

Command Permission Description
/admin role add ALL_ADMINS Add a new dynamic role
/admin role remove ALL_ADMINS Remove a dynamic role
/admin role update ALL_ADMINS Update a role's Discord ID
/admin role list ALL_ADMINS, ALL_MODS List configured roles
/admin role reload ALL_ADMINS Force reload from DB

Deployment Steps

  1. Run the Alembic migration to create the dynamic_role table
  2. Deploy the new code (env vars still work as fallback)
  3. Run ENV_PATH=/path/to/.env python -m scripts.seed_dynamic_roles to populate DB from existing env vars
  4. Verify bot works from DB
  5. (Follow-up PR) Remove Optional fallback fields and env vars

Test plan

  • All 262 tests pass
  • Verify bot starts with existing env vars and empty DB table (fallback works)
  • Run seed script against staging env
  • Test /admin role add, /admin role list, /admin role remove in Discord
  • Test /join autocomplete works
  • Test /verifycertification with a valid cert
  • Test webhook handlers (rank up, cert awarded, subscription change)
  • Verify graceful degradation when a role is missing from DB

dimoschi added 7 commits April 3, 2026 11:00
Introduces the DynamicRole SQLAlchemy model with RoleCategory enum
to store dynamic Discord roles (ranks, certs, seasons, subscriptions,
creators, positions, joinable) in the database instead of requiring
env vars. Includes cert_full_name and cert_integer_id columns for
academy certs to eliminate hardcoded platform mappings.
…back

In-memory cache loaded from DB with dual-read fallback to env vars
during transition. Provides typed lookups (cert by ID, by full name,
by abbreviation), cross-category search (get_post_or_rank), group
lookups, joinable role queries, and CRUD operations. Handles CBBH
legacy alias and explicit search priority for cross-category lookups.
…dition

Load RoleManager synchronously in __main__.py before starting the
webhook server or Discord bot, ensuring roles are available before
any webhook can arrive. Add role_manager attribute to Bot and refresh
cache on Discord reconnects via on_ready() with error handling.
Make dynamic role fields Optional in Roles class (dual-read fallback).
Remove AcademyCertificates class, roles_to_join, dynamic role_groups,
and helper methods from Global (moved to RoleManager). Update all
consumers: verification.py (thread role_manager, replace hardcoded
cert mapping), mp.py, academy.py, verify.py (add None guard), user.py
(autocomplete for /join and /leave instead of static choices).
Slash command group with add/remove/update/list/reload subcommands.
Write operations restricted to ALL_ADMINS, list also available to
ALL_MODS. Auto-reloads RoleManager cache after mutations.
One-time migration tool that reads ROLE_* env vars and inserts them
as DynamicRole rows with correct categories, display names, cert
metadata, and joinable role descriptions.
Add MockRoleManager to test helpers, update MockBot with role_manager
attribute. Rewrite webhook handler tests (mp, academy) to set up
role_manager on bot instead of patching settings. Update verification
tests to use test role IDs via MockRoleManager. Update config tests
for new Optional fields and removed role_groups. Fix pre-existing
role_data KeyError for py-cord compatibility.
@dimoschi dimoschi requested a review from makelarisjr as a code owner April 3, 2026 08:40
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 89.03509% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.76%. Comparing base (b0eb0e1) to head (a6ceb1f).

Files with missing lines Patch % Lines
src/helpers/verification.py 63.15% 14 Missing ⚠️
src/bot.py 16.66% 5 Missing ⚠️
src/cmds/core/admin.py 71.42% 4 Missing ⚠️
src/webhooks/handlers/mp.py 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #179      +/-   ##
==========================================
+ Coverage   62.10%   64.76%   +2.66%     
==========================================
  Files          50       53       +3     
  Lines        2953     3057     +104     
==========================================
+ Hits         1834     1980     +146     
+ Misses       1119     1077      -42     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dimoschi added 3 commits April 3, 2026 14:02
- Add tests for UserCog: autocomplete, join/leave with role_manager
- Add tests for VerifyCog: cert_role not configured, empty certid, bad prefix
- Create test_role_admin.py with full coverage for add/remove/update/list/reload
Change from /role-admin {add|remove|update|list|reload} to
/admin role {add|remove|update|list|reload} to allow future admin
subcommands like /admin config, /admin user, etc.
Create src/cmds/core/admin.py to define the top-level /admin command
group. RoleAdminCog now imports from admin.py and adds the "role"
subgroup via create_subgroup(). This allows other cogs to easily add
their own /admin subcommands by importing the admin group.
@dimoschi dimoschi requested a review from 0xRy4n April 3, 2026 11:27
@0xRy4n
Copy link
Copy Markdown
Member

0xRy4n commented Apr 3, 2026

Looks good to me, but I think we are missing the alembic migration for DynamicRole?

dimoschi added 3 commits April 3, 2026 15:04
…d script

- Remove version attribute from docker-compose.yml (obsolete in Compose spec)
- Change seed script to use MariaDB upsert (INSERT ... ON DUPLICATE KEY UPDATE)
  so it can safely be re-run without errors on the unique constraint
- Add migration to align htb_discord_link columns with models
- Add migration to create dynamic_role table for DB-backed roles
- Remove deprecated 'version' key from docker-compose.yml
- Replace healthcheck.sh with explicit mariadb-admin ping
- Increase start_period to 30s for slower startup
@dimoschi dimoschi requested a review from 0xRy4n April 3, 2026 14:56
@dimoschi dimoschi merged commit 44effd3 into main Apr 6, 2026
7 checks passed
@dimoschi dimoschi deleted the feature/dynamic-roles-db branch April 6, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants